import csv
from typing import Dict, Any

import numpy as np
import pandas as pd
import pyqtgraph as pg
from hashlib import md5
import requests
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtWidgets import QDesktopWidget
from tzlocal import get_localzone

from vnsumscope.event import Event, EventEngine
from vnsumscope.database.sqlite import records_manager
from vnsumscope.trader.ui import QtCore, QtWidgets, QtGui
from vnsumscope.trader.ui.widget import BaseCell, DirectionCell, EnumCell
from pathlib import Path


class BacktestRecord(QtWidgets.QWidget):
    """"""
    signal_log = QtCore.pyqtSignal(Event)

    def __init__(self, record_id):
        """"""
        super().__init__()
        self.record_id = record_id

        self.init_ui()
        self.daily_result = []

    def init_ui(self):
        """"""
        self.setWindowTitle("回测记录")

        self.order_button = QtWidgets.QPushButton("委托记录")
        self.order_button.clicked.connect(self.show_backtesting_orders)

        self.trade_button = QtWidgets.QPushButton("成交记录")
        self.trade_button.clicked.connect(self.show_backtesting_trades)

        self.daily_button = QtWidgets.QPushButton("每日盈亏")
        self.daily_button.clicked.connect(self.show_daily_results)

        for button in [
            self.order_button,
            self.trade_button,
            self.daily_button,
        ]:
            button.setFixedHeight(button.sizeHint().height() * 2)


        record_info = self.get_record_info()
        settings = record_info['settings']
        parameters = record_info['params']
        statistics = record_info['statistics']

        self.settings_monitor = SettingsMonitor()
        self.settings_monitor.setMinimumHeight(400)
        self.settings_monitor.set_data(settings)

        self.parameters_monitor = ParametersMonitor(
            dict(zip(parameters.keys(), parameters.keys()))
        )
        self.parameters_monitor.set_data(parameters)

        self.statistics_monitor = StatisticsMonitor()
        self.statistics_monitor.setMinimumHeight(400)
        self.statistics_monitor.set_data(statistics)

        space_line = QtWidgets.QLabel('')
        left_vbox = QtWidgets.QVBoxLayout()
        settings_title = QtWidgets.QLabel('策略配置')
        settings_title.setAlignment(QtCore.Qt.AlignCenter)
        left_vbox.addWidget(settings_title)
        left_vbox.addWidget(self.settings_monitor)

        params_title = QtWidgets.QLabel('策略参数')
        params_title.setAlignment(QtCore.Qt.AlignCenter)
        left_vbox.addWidget(params_title)
        left_vbox.addWidget(self.parameters_monitor)

        left_vbox.addWidget(space_line)
        left_vbox.addWidget(self.order_button)
        left_vbox.addWidget(self.trade_button)
        left_vbox.addWidget(self.daily_button)

        self.chart = BacktesterChart()
        self.chart.setMinimumWidth(1000)
        self.thread = GetDailyResult(self.record_id)
        self.thread.start()
        self.thread.signal.connect(self.show_chart)

        self.trade_dialog = BacktestingResultDialog(
            "回测成交记录",
            BacktestingTradeMonitor
        )
        self.order_dialog = BacktestingResultDialog(
            "回测委托记录",
            BacktestingOrderMonitor
        )
        self.daily_dialog = BacktestingResultDialog(
            "回测每日盈亏",
            DailyResultMonitor
        )

        vbox = QtWidgets.QVBoxLayout()
        statistis_title = QtWidgets.QLabel('统计指标')
        statistis_title.setAlignment(QtCore.Qt.AlignCenter)
        vbox.addWidget(statistis_title)
        vbox.addWidget(self.statistics_monitor)

        hbox = QtWidgets.QHBoxLayout()
        hbox.addLayout(left_vbox)
        hbox.addLayout(vbox)
        hbox.addWidget(self.chart)
        self.setLayout(hbox)

    def get_record_info(self):
        return records_manager.get_record(self.record_id)

    def show_backtesting_trades(self):
        """"""
        if not self.trade_dialog.is_updated():
            trades = records_manager.get_trade(self.record_id)
            self.trade_dialog.update_data(trades)

        self.trade_dialog.exec_()

    def show_backtesting_orders(self):
        """"""
        if not self.order_dialog.is_updated():
            orders = records_manager.get_order(record_id=self.record_id)
            self.order_dialog.update_data(orders)

        self.order_dialog.exec_()

    def show_daily_results(self):
        """"""
        if not self.daily_dialog.is_updated():
            self.daily_dialog.update_data(self.daily_result)

        self.daily_dialog.exec_()

    def show_chart(self, df):
        self.chart.init_ui()
        self.chart.set_data(df)
        df.reset_index(inplace=True)
        df['date'] = df['date'].astype('str')
        self.daily_result = df.to_dict('records')

    def show(self):
        """"""
        self.showMaximized()

    def closeEvent(self, event):
        event.accept()


class GetDailyResult(QThread):
    signal = pyqtSignal(pd.DataFrame)

    def __init__(self, record_id):
        super(QThread, self).__init__()
        self.record_id = record_id

    def run(self):
        result_df = records_manager.get_daily_result(record_id=self.record_id)
        self.signal.emit(result_df)


class StatisticsMonitor(QtWidgets.QTableWidget):
    """"""
    KEY_NAME_MAP = {
        "start_date": "首个交易日",
        "end_date": "最后交易日",

        "total_days": "总交易日",
        "profit_days": "盈利交易日",
        "loss_days": "亏损交易日",

        "capital": "起始资金",
        "end_balance": "结束资金",

        "total_return": "总收益率",
        "annual_return": "年化收益",
        "max_drawdown": "最大回撤",
        "max_ddpercent": "百分比最大回撤",

        "total_net_pnl": "总盈亏",
        "total_commission": "总手续费",
        "total_slippage": "总滑点",
        "total_turnover": "总成交额",
        "total_trade_count": "总成交笔数",

        "daily_net_pnl": "日均盈亏",
        "daily_commission": "日均手续费",
        "daily_slippage": "日均滑点",
        "daily_turnover": "日均成交额",
        "daily_trade_count": "日均成交笔数",

        "daily_return": "日均收益率",
        "return_std": "收益标准差",
        "sharpe_ratio": "夏普比率",
        "return_drawdown_ratio": "收益回撤比"
    }

    def __init__(self):
        """"""
        super().__init__()

        self.cells = {}

        self.init_ui()

    def init_ui(self):
        """"""
        self.setRowCount(len(self.KEY_NAME_MAP))
        self.setVerticalHeaderLabels(list(self.KEY_NAME_MAP.values()))

        self.setColumnCount(1)
        self.horizontalHeader().setVisible(False)
        self.horizontalHeader().setSectionResizeMode(
            QtWidgets.QHeaderView.Stretch
        )
        self.setEditTriggers(self.NoEditTriggers)

        for row, key in enumerate(self.KEY_NAME_MAP.keys()):
            cell = QtWidgets.QTableWidgetItem()
            self.setItem(row, 0, cell)
            self.cells[key] = cell

    def clear_data(self):
        """"""
        for cell in self.cells.values():
            cell.setText("")

    def set_data(self, data: dict):
        """"""
        if data:
            data["capital"] = f"{data['capital']:,.2f}"
            data["end_balance"] = f"{data['end_balance']:,.2f}"
            data["total_return"] = f"{data['total_return']:,.2f}%"
            data["annual_return"] = f"{data['annual_return']:,.2f}%"
            data["max_drawdown"] = f"{data['max_drawdown']:,.2f}"
            data["max_ddpercent"] = f"{data['max_ddpercent']:,.2f}%"
            data["total_net_pnl"] = f"{data['total_net_pnl']:,.2f}"
            data["total_commission"] = f"{data['total_commission']:,.2f}"
            data["total_slippage"] = f"{data['total_slippage']:,.2f}"
            data["total_turnover"] = f"{data['total_turnover']:,.2f}"
            data["daily_net_pnl"] = f"{data['daily_net_pnl']:,.2f}"
            data["daily_commission"] = f"{data['daily_commission']:,.2f}"
            data["daily_slippage"] = f"{data['daily_slippage']:,.2f}"
            data["daily_turnover"] = f"{data['daily_turnover']:,.2f}"
            data["daily_trade_count"] = f"{data['daily_trade_count']:,.2f}"
            data["daily_return"] = f"{data['daily_return']:,.2f}%"
            data["return_std"] = f"{data['return_std']:,.2f}%"
            data["sharpe_ratio"] = f"{data['sharpe_ratio']:,.2f}"
            data["return_drawdown_ratio"] = f"{data['return_drawdown_ratio']:,.2f}"

            for key, cell in self.cells.items():
                value = data.get(key, "")
                cell.setText(str(value))


class SettingsMonitor(StatisticsMonitor):
    """"""
    KEY_NAME_MAP = {
        "vt_symbol": "标的代码",
        "interval": "周期",
        "start": "开始日期",
        "end": "结束日期",
        "rate": "手续费率",
        "slippage": "交易滑点",
        "size": "合约乘数",
        "pricetick": "价格跳动",
        "capital": "回测资金"
    }

    def set_data(self, data: dict):
        """"""
        if data:
            for key, cell in self.cells.items():
                value = data.get(key, "")
                cell.setText(str(value))


class ParametersMonitor(SettingsMonitor):
    """"""

    def __init__(self, params={}):
        self.KEY_NAME_MAP = params
        super().__init__()


class FloatCell(BaseCell):
    """
    Cell used for showing pnl data.
    """

    def __init__(self, content, data):
        """"""
        content = f"{content:.2f}"
        super().__init__(content, data)


class BacktestingResultDialog(QtWidgets.QDialog):
    """
    """

    def __init__(
            self,
            title: str,
            table_class: QtWidgets.QTableWidget
    ):
        """"""
        super().__init__()

        self.title = title
        self.table_class = table_class

        self.updated = False

        self.init_ui()

    def init_ui(self):
        """"""
        self.setWindowTitle(self.title)
        self.resize(1100, 600)

        self.table = self.table_class()
        vbox = QtWidgets.QVBoxLayout()
        vbox.addWidget(self.table)

        self.setLayout(vbox)

    def clear_data(self):
        """"""
        self.updated = False
        self.table.setRowCount(0)

    def update_data(self, data: list):
        """"""
        self.updated = True

        data.reverse()
        for obj in data:
            self.table.insert_new_row(obj)

    def is_updated(self):
        """"""
        return self.updated


class BaseMonitor(QtWidgets.QTableWidget):
    """
    Monitor data update in VN Trader.
    """

    event_type: str = ""
    data_key: str = ""
    sorting: bool = False
    headers: Dict[str, dict] = {}

    def __init__(self):
        """"""
        super(BaseMonitor, self).__init__()

        self.cells: Dict[str, dict] = {}

        self.init_ui()
        self.load_setting()

    def __del__(self) -> None:
        """"""
        self.save_setting()

    def init_ui(self) -> None:
        """"""
        self.init_table()
        self.init_menu()

    def init_table(self) -> None:
        """
        Initialize table.
        """
        self.setColumnCount(len(self.headers))

        labels = [d["display"] for d in self.headers.values()]
        self.setHorizontalHeaderLabels(labels)

        self.verticalHeader().setVisible(False)
        self.setEditTriggers(self.NoEditTriggers)
        self.setAlternatingRowColors(True)
        self.setSortingEnabled(self.sorting)

    def init_menu(self) -> None:
        """
        Create right click menu.
        """
        self.menu = QtWidgets.QMenu(self)

        resize_action = QtWidgets.QAction("调整列宽", self)
        resize_action.triggered.connect(self.resize_columns)
        self.menu.addAction(resize_action)

        save_action = QtWidgets.QAction("保存数据", self)
        save_action.triggered.connect(self.save_csv)
        self.menu.addAction(save_action)

    def insert_new_row(self, data: Any):
        """
        Insert a new row at the top of table.
        """
        self.insertRow(0)
        row_cells = {}
        for column, header in enumerate(self.headers.keys()):
            setting = self.headers[header]

            content = data.__getattribute__(header)
            cell = setting["cell"](content, data)
            self.setItem(0, column, cell)

            if setting["update"]:
                row_cells[header] = cell

        if self.data_key:
            key = data.__getattribute__(self.data_key)
            self.cells[key] = row_cells

    def resize_columns(self) -> None:
        """
        Resize all columns according to contents.
        """
        self.horizontalHeader().resizeSections(QtWidgets.QHeaderView.ResizeToContents)

    def save_csv(self) -> None:
        """
        Save table data into a csv file
        """
        path, _ = QtWidgets.QFileDialog.getSaveFileName(
            self, "保存数据", "", "CSV(*.csv)")

        if not path:
            return

        with open(path, "w") as f:
            writer = csv.writer(f, lineterminator="\n")

            headers = [d["display"] for d in self.headers.values()]
            writer.writerow(headers)

            for row in range(self.rowCount()):
                if self.isRowHidden(row):
                    continue

                row_data = []
                for column in range(self.columnCount()):
                    item = self.item(row, column)
                    if item:
                        row_data.append(str(item.text()))
                    else:
                        row_data.append("")
                writer.writerow(row_data)

    def contextMenuEvent(self, event: QtGui.QContextMenuEvent) -> None:
        """
        Show menu with right click.
        """
        self.menu.popup(QtGui.QCursor.pos())

    def save_setting(self) -> None:
        """"""
        settings = QtCore.QSettings(self.__class__.__name__, "custom")
        # settings.setValue("column_state", self.horizontalHeader().saveState())

    def load_setting(self) -> None:
        """"""
        settings = QtCore.QSettings(self.__class__.__name__, "custom")


class BacktestingTradeMonitor(BaseMonitor):
    """
    Monitor for backtesting trade data.
    """
    headers = {
        "tradeid": {"display": "成交号 ", "cell": BaseCell, "update": False},
        "orderid": {"display": "委托号", "cell": BaseCell, "update": False},
        "symbol": {"display": "代码", "cell": BaseCell, "update": False},
        "exchange": {"display": "交易所", "cell": EnumCell, "update": False},
        "direction": {"display": "方向", "cell": DirectionCell, "update": False},
        "offset": {"display": "开平", "cell": EnumCell, "update": False},
        "price": {"display": "价格", "cell": BaseCell, "update": False},
        "volume": {"display": "数量", "cell": BaseCell, "update": False},
        "datetime": {"display": "时间", "cell": BaseCell, "update": False},
        "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
    }


class BacktestingOrderMonitor(BaseMonitor):
    """
    Monitor for backtesting order data.
    """

    headers = {
        "orderid": {"display": "委托号", "cell": BaseCell, "update": False},
        "symbol": {"display": "代码", "cell": BaseCell, "update": False},
        "exchange": {"display": "交易所", "cell": EnumCell, "update": False},
        "type": {"display": "类型", "cell": EnumCell, "update": False},
        "direction": {"display": "方向", "cell": DirectionCell, "update": False},
        "offset": {"display": "开平", "cell": EnumCell, "update": False},
        "price": {"display": "价格", "cell": BaseCell, "update": False},
        "volume": {"display": "总数量", "cell": BaseCell, "update": False},
        "traded": {"display": "已成交", "cell": BaseCell, "update": False},
        "status": {"display": "状态", "cell": EnumCell, "update": False},
        "datetime": {"display": "时间", "cell": BaseCell, "update": False},
        "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
    }


class DailyResultMonitor(BaseMonitor):
    """
    Monitor for backtesting daily result.
    """

    headers = {
        "date": {"display": "日期", "cell": BaseCell, "update": False},
        "trade_count": {"display": "成交笔数", "cell": BaseCell, "update": False},
        "start_pos": {"display": "开盘持仓", "cell": BaseCell, "update": False},
        "end_pos": {"display": "收盘持仓", "cell": BaseCell, "update": False},
        "turnover": {"display": "成交额", "cell": FloatCell, "update": False},
        "commission": {"display": "手续费", "cell": FloatCell, "update": False},
        "slippage": {"display": "滑点", "cell": FloatCell, "update": False},
        "trading_pnl": {"display": "交易盈亏", "cell": FloatCell, "update": False},
        "holding_pnl": {"display": "持仓盈亏", "cell": FloatCell, "update": False},
        "total_pnl": {"display": "总盈亏", "cell": FloatCell, "update": False},
        "net_pnl": {"display": "净盈亏", "cell": FloatCell, "update": False},
    }

    def insert_new_row(self, data: Any):
        """
        Insert a new row at the top of table.
        """
        self.insertRow(0)
        col_num = 0
        for column, cell_setting in self.headers.items():

            content = data.get(column, None)
            cell = cell_setting['cell'](content, str)
            self.setItem(0, col_num, cell)
            col_num += 1


class DateAxis(pg.AxisItem):
    """Axis for showing date data"""

    def __init__(self, dates: dict, *args, **kwargs):
        """"""
        super().__init__(*args, **kwargs)
        self.dates = dates

    def tickStrings(self, values, scale, spacing):
        """"""
        strings = []
        for v in values:
            dt = self.dates.get(v, "")
            strings.append(str(dt))
        return strings


class BacktesterChart(pg.GraphicsWindow):
    """"""

    def __init__(self):
        """"""
        super().__init__(title="Backtester Chart")

        self.dates = {}

    def init_ui(self):
        """"""
        pg.setConfigOptions(antialias=True)

        # Create plot widgets
        self.balance_plot = self.addPlot(
            title="账户净值",
            axisItems={"bottom": DateAxis(self.dates, orientation="bottom")},
            labels={'left': ('金额'), 'bottom': ('日期')}
        )
        self.nextRow()

        self.drawdown_plot = self.addPlot(
            title="净值回撤",
            axisItems={"bottom": DateAxis(self.dates, orientation="bottom")},
            labels={'left': ('金额'), 'bottom': ('日期')}
        )
        self.nextRow()

        self.pnl_plot = self.addPlot(
            title="每日盈亏",
            axisItems={"bottom": DateAxis(self.dates, orientation="bottom")},
            labels={'left': ('金额'), 'bottom': ('日期')}
        )
        self.nextRow()

        self.distribution_plot = self.addPlot(title="盈亏分布", labels={'left': ('百分比'), 'bottom': ('金额')})

        # Add curves and bars on plot widgets
        self.balance_curve = self.balance_plot.plot(
            pen=pg.mkPen("#ffc107", width=3)
        )

        dd_color = "#303f9f"
        self.drawdown_curve = self.drawdown_plot.plot(
            fillLevel=-0.3, brush=dd_color, pen=dd_color
        )

        profit_color = 'r'
        loss_color = 'g'
        self.profit_pnl_bar = pg.BarGraphItem(
            x=[], height=[], width=0.3, brush=profit_color, pen=profit_color
        )
        self.loss_pnl_bar = pg.BarGraphItem(
            x=[], height=[], width=0.3, brush=loss_color, pen=loss_color
        )
        self.pnl_plot.addItem(self.profit_pnl_bar)
        self.pnl_plot.addItem(self.loss_pnl_bar)

        distribution_color = "#6d4c41"
        self.distribution_curve = self.distribution_plot.plot(
            fillLevel=-0.3, brush=distribution_color, pen=distribution_color
        )

    def clear_data(self):
        """"""
        self.balance_curve.setData([], [])
        self.drawdown_curve.setData([], [])
        self.profit_pnl_bar.setOpts(x=[], height=[])
        self.loss_pnl_bar.setOpts(x=[], height=[])
        self.distribution_curve.setData([], [])

    def set_data(self, df):
        """"""
        if df is None:
            return

        count = len(df)

        self.dates.clear()
        for n, date in enumerate(df.index):
            self.dates[n] = date

        # Set data for curve of balance and drawdown
        self.balance_curve.setData(df["balance"])
        self.drawdown_curve.setData(df["drawdown"])

        # Set data for daily pnl bar
        profit_pnl_x = []
        profit_pnl_height = []
        loss_pnl_x = []
        loss_pnl_height = []

        for count, pnl in enumerate(df["net_pnl"]):
            if pnl >= 0:
                profit_pnl_height.append(pnl)
                profit_pnl_x.append(count)
            else:
                loss_pnl_height.append(pnl)
                loss_pnl_x.append(count)

        self.profit_pnl_bar.setOpts(x=profit_pnl_x, height=profit_pnl_height)
        self.loss_pnl_bar.setOpts(x=loss_pnl_x, height=loss_pnl_height)

        # Set data for pnl distribution
        hist, x = np.histogram(df["net_pnl"], bins="auto")
        x = x[:-1]
        self.distribution_curve.setData(x, hist)


class BacktestRecordsManager(QtWidgets.QWidget):
    """"""

    def __init__(self, strategy_path: str):
        """"""
        super().__init__()
        self._strategy_path = strategy_path
        self.init_ui()
        self.resize(900,600)
        self.center()

    def init_ui(self) -> None:
        """"""
        self.setWindowTitle("回测记录")
        self.init_table()

        hbox1 = QtWidgets.QHBoxLayout()
        hbox1.addWidget(self.table)
        vbox = QtWidgets.QVBoxLayout()
        vbox.addLayout(hbox1)
        self.setLayout(vbox)

    def center(self):  # 定义一个函数使得窗口居中显示
        # 获取屏幕坐标系
        screen = QDesktopWidget().screenGeometry()
        # 获取窗口坐标系
        size = self.geometry()
        newLeft = (screen.width() - size.width()) / 2
        newTop = (screen.height() - size.height()) / 2
        self.move(int(newLeft), int(newTop))

    def init_table(self) -> None:
        """"""
        labels = [
            "序号",
            "品种代码",
            "状态",
            "总收益率",
            "创建时间",
            "操作",
        ]
        self.table = QtWidgets.QTableWidget()
        self.showMaximized()
        self.table.setColumnCount(len(labels))
        self.table.setHorizontalHeaderLabels(labels)
        self.table.verticalHeader().setVisible(False)
        self.table.horizontalHeader().setSectionResizeMode(
            QtWidgets.QHeaderView.ResizeToContents
        )
        self.show_data()

    def show_data(self) -> None:
        """"""
        # 获取策略名
        strategy_name = Path(self._strategy_path).stem
        # md5加密获得策略id
        strategy_id = md5(strategy_name.encode('utf8')).hexdigest()
        records = records_manager.get_records(strategy_id)
        self.table.setRowCount(0)
        self.table.setRowCount(len(records))
        records_status = {
            1:'RUNNING',
            2:'SUCCESS',
            3:'FAILED'
        }

        for row, record in enumerate(records):
            self.table.setItem(row, 0, DataCell(str(row+1)))
            self.table.setItem(row, 1, DataCell(record.get('symbol')))
            self.table.setItem(row, 2, DataCell(records_status.get(record.get('status'))))
            self.table.setItem(row, 3, DataCell(str(round(record.get('total_return'), 6))))
            self.table.setItem(row, 4, DataCell(record.get('create_time').strftime('%Y-%m-%d %H:%M:%S')))
            self.table.setCellWidget(row, 5, ButtonWidget('查看', record_id=record.get('id')))


class ButtonWidget(QtWidgets.QWidget):
    """将行信息和事件绑定到button上，便于查到表格对应的行打开对应窗口"""
    def __init__(self, text, record_id,  *args, **kwargs) -> None:
        super(ButtonWidget, self).__init__(*args, **kwargs)
        self.text = text
        self.record_id = record_id
        layout = QtWidgets.QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(QtWidgets.QPushButton(self.text, self, clicked=self.clicked_trigger))
        self.widgets: Dict[str, QtWidgets.QWidget] = {}

    def clicked_trigger(self) -> None:
        if self.text == '查看':
            self.view_record()

    def view_record(self) -> None:
        widget = BacktestRecord(self.record_id)
        self.widgets[str(self.record_id)] = widget
        widget.show()

class DataCell(QtWidgets.QTableWidgetItem):
    """"""

    def __init__(self, text: str = ""):
        super().__init__(text)

        self.setTextAlignment(QtCore.Qt.AlignCenter)


if __name__ == "__main__":
    from vnsumscope.trader.ui import create_qapp
    app = create_qapp()
    brm = BacktestRecord(record_id=2)
    brm.show()
    app.exec_()
